SwaggerをAWS Serverless Application Modelから扱う方法を理解する
はじめに
こんにちは、中山です。
最近SwaggerをAWS Serverless Application Model(以下AWS SAM)から利用する機会がありました。AWS SAMではAWS::Serverless::ApiまたはAWS::ApiGateway::RestApiリソースを利用することにより、Swagger形式で記述されたファイルを扱うことが可能です。基本的にAWS SAMのみで利用可能な AWS::Serverless::Api
リソースを利用することが多いはずです。API Gatewayは複数のリソースに分離されているため、AWS SAMのベースとなっているCloudFromation用のリソースを使うと、テンプレートの行数が長くなってしまうからです。
執筆時点(2017/03/08)では AWS::Serverless::Api
リソースはSwaggerファイルを以下2つの方法で定義できます。
- Swaggerを外部ファイルに記述する
- Swaggerをインラインで記述する
それぞれメリット/デメリットがあります。本エントリでそれぞれの違いをご紹介したいと思います。
なお、本エントリを執筆する上で検証に利用した主要な各種ツールのバージョンは以下の通りです。バージョンによって結果が変更される可能性があるので、その点ご了承ください。
- AWS SAM: 2016-10-31
- AWS CLI: aws-cli/1.11.58 Python/2.7.12 Darwin/16.4.0 botocore/1.5.21
Swaggerを外部ファイルに記述する場合
AWS SAMのテンプレートと分けた形でSwaggerを管理したい場合、 AWS::Serverless::Api
リソースの DefinitionUri
プロパティを利用します。このプロパティの値にS3へアップロードされたSwaggerファイルへのパスを記述することで、その内容をもとにAPI Gatewayの設定を行うことが可能です。指定方法は以下の2つがあります。
DefinitionUri
にS3またはローカルファイルへのパスを指定する- S3 Location Object形式でSwaggerファイルが設置されたバケット名とキーを指定する
1点目について。 DefinitionUri
にSwaggerファイルが設置されているS3へのパスを指定する方式です。例えば、以下のように記述できます。
Api: Type: AWS::Serverless::Api Properties: DefinitionUri: s3://<_YOUR_S3_BUCKET_>/swagger.yml
ただし、S3へのパスを直接記述してしまうとSwaggerの更新毎にファイルをアップロードしなければならず、少し面倒です。この問題に対して、 aws cloudformation package
コマンドを利用することにより、ローカルファイルへのパスを指定可能です。指定されたファイルはこのコマンドによってS3へ自動的にアップロードされます。例えば以下のように記述できます。
Api: Type: AWS::Serverless::Api Properties: DefinitionUri: src/api/swagger.yml
ただし、この形式でSwaggerを指定する場合、CloudFromationの組み込み関数は現状利用できないという点は注意してください。例えば、Swaggerを開発/本番環境で分けたいと考えた時( swagger.dev.yml
/ swagger.prod.yml
など)、以下にように記述することはできません。 Env
はパラメータで渡された変数です。
Api: Type: AWS::Serverless::Api Properties: DefinitionUri: !Sub src/api/swagger.${Env}.yml
以下のようなエラーが出力されてしまいます。
Failed to create the changeset: Waiter ChangeSetCreateComplete failed: Waiter encountered a terminal failure state Status: FAILED. Reason: Transform AWS::Serverless-2016-10-31 failed with: Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [Api] is invalid. 'DefinitionUri' requires Bucket and Key properties to be specified
続いて2点目について。 DefinitionUri
はS3 Location Object形式での記述方法にも対応しています。例えば以下のように指定可能です。
Api: Type: AWS::Serverless::Api Properties: DefinitionUri: Bucket: !Ref S3Bucket Key: swagger.yml
上記のようにこの形式の場合、組み込み関数が最近サポートされました。このアップデートにより、より柔軟な形でテンプレートを記述できるようになったのは嬉しいポイントです。ただし、1つ目の方法のように aws cloudformation package
コマンドでローカルファイルをS3にアップロードする機能はありません。スタックの作成/更新前に自分でアップロードしておく必要があります。
Swaggerを外部ファイルに記述する場合の変数について
この形式でSwaggerを扱う場合、注意する点があります。それはSwaggerファイル内で扱える変数に一部制限があるという点です。Swagger内で変数を扱うには、 AWS::Serverless::Api
リソースの Variables
プロパティにステージ変数を設定します。例えば、AWS SAMで定義したLambda関数名をSwagger内で変数として扱いたい場合、以下のように記述できます。
Api: Type: AWS::Serverless::Api Properties: Variables: FuncName: !Ref Func1 <snip> Func1: Type: AWS::Serverless::Function
Ref組み込み関数でAWS::Serverless::Functionリソースを参照することにより、関数名を FuncName
変数に代入しています。Swaggerファイルからは ${stageVariables.FuncName}
という書式で変数を参照可能です。
一見するとステージ変数を使えば何でも変数に代入できるように思えますが、現時点ではSwagger内で頻繁に利用されるであろうAWSリージョン及びAWSアカウントIDは参照できません。こちらのイシューでも言及されています。例えば、API GatewayのバックエンドにLambda関数を指定する場合、 x-amazon-apigateway-integration
オブジェクトなどの uri
プロパティで、以下のような形式でLambda関数を指定する必要があります。
x-amazon-apigateway-integration: responses: default: statusCode: 200 uri: arn:aws:apigateway:<_YOUR_AWS_REGION_>:lambda:path/2015-03-31/functions/arn:aws:lambda:<_YOUR_AWS_REGION_>:<_YOUR_AWS_ACCOUNT_ID_>:function:<_YOUR_LAMBDA_FUNCTION_NAME_>/invocations passthroughBehavior: when_no_templates httpMethod: POST type: aws
ハイライトしている箇所に注目してください。バックエンドのLambda関数を指定する際に、AWSリージョン及びAWSアカウントIDを指定しなければならないことが分かります。実際どういったエラーになるのか確認するため、例えば以下のように無理やりステージ変数にこれらの値を代入させたとします。
Api: Type: AWS::Serverless::Api Properties: DefinitionUri: src/api/swagger.yml StageName: !Ref Env Variables: FuncName: !Ref Func1 AWSRegion: !Ref AWS::Region AWSAccountId: !Ref AWS::AccountId
API Gatewayの内容を確認するとステージ変数自体には期待した値が代入されています。
$ aws apigateway get-stage \ --stage-name dev \ --rest-api-id "$(aws apigateway get-rest-apis \ --query 'items[?contains(name, `swagger-external`)].id' \ --output text)" { "stageName": "dev", "variables": { "AWSAccountId": "************", "AWSRegion": "ap-northeast-1", "FuncName": "swagger-external-dev-Func1-OENQS4JL6URJ" }, "cacheClusterEnabled": false, "cacheClusterStatus": "NOT_AVAILABLE", "deploymentId": "ejx35f", "lastUpdatedDate": 1488952223, "createdDate": 1488952222, "methodSettings": {} }
この状態で uri
プロパティを以下のように変更しても期待した動作はしません。
x-amazon-apigateway-integration: responses: default: statusCode: 200 uri: arn:aws:apigateway:${stageVariables.AWSRegion}:lambda:path/2015-03-31/functions/arn:aws:lambda:${stageVariables.AWSRegion}:${stageVariables.AWSAccountId}:function:${stageVariables.FuncName}/invocations passthroughBehavior: when_no_templates httpMethod: POST type: aws
以下のようなエラーが出力されます。
Errors found during import: Unable to put integration on 'GET' for resource at path '/': Invalid lambda function
ちなみに、AWSリージョンとAWSアカウントIDが含まれるLambda関数のARNを渡したとしても同様のエラーが出ます。
Swaggerをインラインで記述する場合
2017年3月31追記
AWS::Includeを利用することで、インラインで定義したSwaggerファイルとAWS SAMで定義したテンプレートを分離可能です。こちらのエントリにまとめました。
続いてAWS SAMのテンプレート内にインラインでSwaggerを記述する方法についてご紹介します。最近のアップデートで対応した機能です。 AWS::Serverless::Api
リソースの DefinitionBody
プロパティを利用することにより、例えば以下のような記述が可能です。
Api: Type: AWS::Serverless::Api Properties: StageName: !Ref Env DefinitionBody: swagger: 2.0 info: title: !Sub swagger-external-${Env} description: !Sub swagger-external-${Env} version: 1.0.0 schemes: - https basePath: !Sub /${Env} paths: /: get: summary: Root description: | Root Method. consumes: - application/json produces: - application/json parameters: - name: number in: query description: Some number required: true type: number format: integer responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" x-amazon-apigateway-integration: responses: default: statusCode: 200 uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Func1.Arn}/invocations passthroughBehavior: when_no_templates httpMethod: POST type: aws requestTemplates: application/json: | { #foreach($key in $input.params().querystring.keySet()) "$key": "$util.escapeJavaScript($input.params().querystring.get($key))" #if($foreach.hasNext),#end #end } definitions: Empty: type: object title: Empty Schema Func1: Type: AWS::Serverless::Function
この方式の利点は何と言ってもAWS SAMの機能をそのまま利用できるという点です。つまり、組み込み関数が使えるのでより柔軟な形でテンプレートを記述できます。ただし、外部ファイルに記述する方式であればAWS SAM以外でも利用しているSwaggerファイルを基本的にそのまま再利用することはできますが、こちらの方式の場合AWS SAM以外で管理できなくなるという点は注意してください。AWS SAMの機能に依存している分ポータビリティは低くなります。
AWS SAMのネストスタックについて
インライン方式の場合、Swaggerの内容をAWS SAMテンプレートに記述する必要があるためどうしてもテンプレートの内容が長くなってしまいます。CloudFromationにはAWS::CloudFormation::Stackリソースでスタックをネストさせることができるので、大本となるAWS SAMとSwaggerを記述した2つのテンプレートに分ければこの問題を解決できそうです。が、「社会は甘くない」のでそう簡単にはいきません。
例えば、以下のようなディレクトリ構成にしたとします。
$ tree . ├── sam.yml └── src ├── handlers │ └── func1 │ └── index.py └── templates └── swagger.yml 4 directories, 3 files
各種テンプレートは以下の内容です。
sam.yml
--- AWSTemplateFormatVersion: 2010-09-09 Transform: AWS::Serverless-2016-10-31 Description: swagger-inline-2 Parameters: Env: Type: String Default: dev Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: General Configuration Parameters: - Env ParameterLabels: Env: default: Env Resources: Api: Type: AWS::CloudFormation::Stack Properties: TemplateURL: src/templates/swagger.yml Parameters: Env: !Ref Env FuncArn: !GetAtt Func1.Arn Func1: Type: AWS::Serverless::Function Properties: CodeUri: src/handlers/func1 Handler: index.handler Runtime: python2.7 Events: PostApi: Type: Api Properties: Path: / Method: GET RestApiId: !Ref Api.Outputs.Name
src/templates/swagger.yml
--- AWSTemplateFormatVersion: 2010-09-09 Transform: AWS::Serverless-2016-10-31 Description: swagger Parameters: Env: Type: String Default: dev FuncArn: Type: String Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: General Configuration Parameters: - Env - FuncArn ParameterLabels: Env: default: Env FuncArn: default: Function Arn Resources: Api: Type: AWS::Serverless::Api Properties: StageName: !Ref Env DefinitionBody: swagger: 2.0 info: title: !Sub swagger-internal-${Env} description: !Sub swagger-internal-${Env} version: 1.0.0 schemes: - https basePath: !Sub /${Env} paths: /: get: summary: Root description: | Root Method. consumes: - application/json produces: - application/json parameters: - name: number in: query description: Some number required: true type: number format: integer responses: "200": description: 200 response schema: $ref: "#/definitions/Empty" x-amazon-apigateway-integration: responses: default: statusCode: 200 uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${FuncArn}/invocations passthroughBehavior: when_no_templates httpMethod: POST type: aws requestTemplates: application/json: | { #foreach($key in $input.params().querystring.keySet()) "$key": "$util.escapeJavaScript($input.params().querystring.get($key))" #if($foreach.hasNext),#end #end } definitions: Empty: type: object title: Empty Schema Outputs: Name: Value: !Ref Api
両テンプレートともAWS SAMの機能を利用しているため、 AWSTemplateFormatVersion: 2010-09-09
という記述をし、テンプレートがCloudFromationではなくAWS SAMであることを明示的に指定する必要があります。一見よさそうですが、このテンプレート動きません。。。以下2つの問題があります。
- ネストされたAWS SAMテンプレートは
CreateStack
APIに対応していない Events
プロパティのRestApiId
で指定するAPI Gatewayのリソース名を別のスタックから直接参照できない
1点目について。こちらのイシューでも指摘されていますが、現時点ではネストされたテンプレートがAWS SAMの場合、 CreateStack
APIは実行できません。以下のようなエラーが出力されてしまいます。
CreateStack cannot be used with templates containing Transforms.,
そのため、一度 CreateChangeSet
してから ExecuteChangeSet
する必要があります。幸い、AWS SAMのスタックを作成する際に利用することになる aws cloudformation deploy
コマンドには --no-execute-changeset
オプションというChangeSetのみ作成してくれる機能があります。これを使えば問題を解消できるかと思ったのですが、私が検証した限りだと ExecuteChangeSet
を実施しても、結局内部的には CreateStack
しているため、同じエラーが出力されてしまいました。。。
続いて2点目について。先程のテンプレートでは src/templates/swagger.yml
のアウトプットで AWS::Serverless::Api
リソースのリソース名を sam.yml
から参照できるようにしていました。しかし、このテンプレートからスタックを作成しようとすると以下のようなエラーが表示されます。
Failed to create the changeset: Waiter ChangeSetCreateComplete failed: Waiter encountered a terminal failure state Status: FAILED. Reason: Transform AWS::Serverless-2016-10-31 failed with: Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [Func1] is invalid. Event with id [PostApi] is invalid. RestApiId property of Api event must reference a valid resource in the same template.
src/templates/swagger.yml
を別テンプレートとして切り出してFn::ImportValue関数を利用したスタック間参照なら対応できるかと思ったのですが、それもダメそうです。ドキュメントに現時点では対応していないと書かれています。以下に引用します。
ImportValue allows one stack to refer to value of properties from another stack. ImportValue is supported on most properties, except the very few that SAM needs to parse. Following properties are not supported:
RestApiId of AWS::Serverless::Function
Policies of AWS::Serverless::Function
StageName of AWS::Serverless::Api
パラメータで渡す方式なら対応できそうですが、そうするとスタックを別々に作成しなければならず、スタックの管理が煩雑になると思います。そのため、インライン方式を採用する場合はテンプレートの行数が長くなることは覚悟しておいた方が良さそうです。
まとめ
いかがだったでしょうか。
AWS SAMからSwaggerを扱う方法をご紹介しました。いろいろ注意点などはありますが、Swaggerをそのまま利用できるというのは非常に便利だと思います。どういった形でSwaggerとAWS SAMを管理したいのか方針を決めてから、ご自身の環境にあう方法を見つけるとよいかと思います。
本エントリがみなさんの参考になれば幸いに思います。